package com.fmy.mcsecurity.token.impl;

import cn.hutool.core.util.StrUtil;
import com.fmy.mcsecurity.commons.tools.SecurityTool;
import com.fmy.mcsecurity.token.TokenStorage;
import com.fmy.mcsecurity.token.model.RefreshStorage;
import com.fmy.mcsecurity.token.model.SecurityToken;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author Mr.fmy
 * @version 1.0.0
 * @ClassName RedisTokenGenerate
 * @Description TODO redis方案
 * @createTime 2019/12/11 15:00
 */
public class RedisTokenStorage implements TokenStorage {

    private RedisTemplate<String, Object> redisTemplate;
    private int concurrency = 1;

    public RedisTokenStorage(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public RedisTokenStorage(RedisTemplate<String, Object> redisTemplate, int concurrency) {
        this.redisTemplate = redisTemplate;
        this.concurrency = concurrency;
    }

    /**
     * k 储存 token
     * v 储存 user_key
     */
    private final String REDIS_TOKEN = "redis_token";
    /**
     * k 储存 refresh_token
     * v 储存 RefreshStorage
     */
    private final String REDIS_REFRESH_TOKEN = "redis_refresh_token";

    /**
     * k 储存 token
     * v 储存 剔出信息
     */
    private final String REDIS_KICK_TOKEN = "redis_kick_token";

    /**
     * k 储存 user_key
     * v 储存 Authentication
     */
    private final String REDIS_AUTHENTICATION = "redis_authentication";

    /**
     * k 储存 user_key
     * v 储存 Map<SecurityToken> 0~N 对应 最先登录~最后登录
     */
    private final String REDIS_USER_CONCURRENCY = "redis_user_concurrency";

    @Override
    public String getUserKey(String token) {
        Object o = redisTemplate.opsForValue().get(keyBefore(REDIS_TOKEN, token));
        return o != null ? o.toString() : null;
    }

    @Override
    public Authentication getAuthentication(String userKey) {
        Object o = redisTemplate.opsForValue().get(keyBefore(REDIS_AUTHENTICATION, userKey));
        if (o instanceof Authentication) {
            return (Authentication) o;
        }
        return null;
    }

    @Override
    public void updateAuthentication(String userKey, Authentication authentication) {
        String authentication_key = keyBefore(REDIS_AUTHENTICATION, userKey);
        Boolean aBoolean = redisTemplate.hasKey(authentication_key);
        if (aBoolean != null && aBoolean) {
            redisTemplate.opsForValue().set(authentication_key, authentication);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void setAuthentication(String client, String type, String userKey, Authentication authentication, SecurityToken token) {
        String authentication_key = keyBefore(REDIS_AUTHENTICATION, userKey);
        String token_key = keyBefore(REDIS_TOKEN, token.getToken());
        String refresh_token = keyBefore(REDIS_REFRESH_TOKEN, token.getRefreshToken());
        String redis_user_concurrency = keyBefore(REDIS_USER_CONCURRENCY, userKey);
        redisTemplate.execute(new SessionCallback<Object>() {
            @Nullable
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                try {
                    RefreshStorage refreshStorage = new RefreshStorage(SecurityTool.getUserId(type, userKey), client, type);
                    operations.opsForValue().set(authentication_key, authentication, token.getTokenExpire(), token.getTimeUnit());
                    operations.opsForValue().set(token_key, userKey, token.getTokenExpire(), token.getTimeUnit());
                    operations.opsForValue().set(refresh_token, refreshStorage, token.getRefreshTokenExpire(), token.getTimeUnit());
                    operations.opsForList().rightPush(redis_user_concurrency, token);
                    operations.expire(redis_user_concurrency, token.getTokenExpire(), token.getTimeUnit());
                    return operations.exec();
                } catch (Exception e) {
                    operations.discard();
                    throw e;
                }
            }
        });

    }

    @Override
    public List<SecurityToken> getToken(String userKey) {
        String redis_user_concurrency = keyBefore(REDIS_USER_CONCURRENCY, userKey);
        List<Object> o = redisTemplate.opsForList().range(redis_user_concurrency, 0, -1);
        if (o == null) {
            return null;
        }
        List<SecurityToken> result = new ArrayList<>(o.size());
        o.forEach(i -> {
            if (i instanceof SecurityToken) {
                result.add((SecurityToken) i);
            }
        });
        return result;
    }

    @Override
    public RefreshStorage getRefreshTokenStorage(String refreshtoken) {
        String refresh_token = keyBefore(REDIS_REFRESH_TOKEN, refreshtoken);
        Object o = redisTemplate.opsForValue().get(refresh_token);
        if (o != null & o instanceof RefreshStorage) {
            return (RefreshStorage) o;
        }
        return null;
    }

    @Override
    public void kick(String token, String cause) {
        kick(getUserKey(token), token, cause);
    }

    @Override
    public void kickUserKey(String userKey, String cause) {
        kick(userKey, null, cause);
    }

    @Override
    public String getkickCause(String token) {
        String kick_token = keyBefore(REDIS_KICK_TOKEN, token);
        Object o = redisTemplate.opsForValue().get(kick_token);
        return o != null ? o.toString() : "";
    }

    @Override
    public void setAuthenticationConcurrency(String client, String type, String userKey, Authentication authentication,
                                             SecurityToken token) {
        int concurrency = concurrency();
        String redis_user_concurrency = keyBefore(REDIS_USER_CONCURRENCY, userKey);
        Long size = redisTemplate.opsForList().size(redis_user_concurrency);
        if (size != null && size >= concurrency) {
            long end = (size - concurrency);
            List<Object> tokens = redisTemplate.opsForList().range(redis_user_concurrency, 0, end);
            if (tokens != null) {
                tokens.forEach(t -> {
                    if (t instanceof SecurityToken) {
                        kick(((SecurityToken) t).getToken(), "您的账号在别处登录，如不是本人操作，请及时修改密码");
                    }
                });
            }
        }
        setAuthentication(client, type, userKey, authentication, token);
    }

    @Override
    public int concurrency() {
        return concurrency;
    }

    private String keyBefore(String before, String v) {
        return before + "_" + v;
    }

    /**
     * 剔出
     *
     * @param userKey * user_key
     * @param token   如果 token存在，那么只删除集合中的token，如果该token不存在，那么将删除user_key
     * @param cause   * 剔出理由
     */
    @SuppressWarnings("unchecked")
    private void kick(String userKey, String token, String cause) {
        if (StrUtil.isNotBlank(userKey)) {
            String authentication_key = keyBefore(REDIS_AUTHENTICATION, userKey);
            String redis_user_concurrency = keyBefore(REDIS_USER_CONCURRENCY, userKey);
            List<Object> tokens = redisTemplate.opsForList().range(redis_user_concurrency, 0, -1);
            if (tokens != null && tokens.size() > 0) {
                redisTemplate.execute(new SessionCallback<Object>() {
                    @Nullable
                    @Override
                    public Object execute(RedisOperations operations) throws DataAccessException {
                        try {
                            operations.multi();
                            for (Object item : tokens) {
                                if (item instanceof SecurityToken) {
                                    SecurityToken sToken = (SecurityToken) item;
                                    String delToken = sToken.getToken();
                                    if (sToken.getToken().equals(token)) {
                                        delToken = token;
                                    } else if (StrUtil.isNotBlank(token)) {
                                        continue;
                                    }
                                    String token_key = keyBefore(REDIS_TOKEN, delToken);
                                    String refresh_token = keyBefore(REDIS_REFRESH_TOKEN, sToken.getRefreshToken());
                                    String kick_token = keyBefore(REDIS_KICK_TOKEN, delToken);
                                    //移除 refresh_token->RefreshStorage，token->user_key
                                    operations.delete(Arrays.asList(token_key, refresh_token));
                                    //移除 user_key->SecurityToken
                                    operations.opsForList().remove(redis_user_concurrency, 0, sToken);
                                    if (tokens.size() == 1 || StrUtil.isBlank(token)) {
                                        //只有当清除全部 userKey 或者 当前登录人只有一个的时候才会清除 authentication_key
                                        operations.delete(authentication_key);
                                    }
                                    //记录剔出原因
                                    operations.opsForValue().set(kick_token, cause, 12L, TimeUnit.HOURS);
                                    if (StrUtil.isNotBlank(token)) {
                                        break;
                                    }
                                }
                            }
                            return operations.exec();
                        } catch (Exception e) {
                            operations.discard();
                            throw e;
                        }
                    }
                });
            }
        }
    }
}
